package no.ffi.gismoidm.invocations;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.PublicKey;
import java.util.UUID;
import no.ffi.gismoidm.attrex.BooleanOperand;
import no.ffi.gismoidm.attrex.TrueAlways;
import no.ffi.gismoidm.servicestubs.SubjectProperties;
import no.ffi.gismoidm.utils.IdentityStatement;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.SignedObject;

/**
 * This class holds the necessary elements for an authenticated
 * service invocation: Identitystatement, Parameters/Returns,
 * Nonce and Signature
 *
 * @author anders
 */
public class ServiceInvocationStateful implements ServiceInvocation {

    private static int MAX_CLOCK_SKEW = 300000; // TODO Config variable perhaps?
    transient ServiceInvocationInternalObject so;
    IdentityStatement identityStatement,crossIdentityStatement;
    SignedObject blob;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        so = (ServiceInvocationInternalObject)blob.getObject();
    }

    // Constructor for request objects (Class access)
    ServiceInvocationStateful(Serializable parameters, IdentityStatement localIS, CryptoOperations crOp,long clockCounter) {
        this(parameters,null,localIS,null,null,crOp,clockCounter);
        try{
            System.out.println("Setter timestamp til " + so.issuingTimestamp);
            Thread.sleep(000);
        } catch (Exception e) {}
        System.out.println("Timestamp on Service Invocation object is " + so.issuingTimestamp);
    }

    // Constructor for response objects
    ServiceInvocationStateful(Serializable returns, IdentityStatement serverID, UUID nonce, CryptoOperations crOp) {
        this(null,returns,serverID,null,nonce,crOp,Long.MAX_VALUE);
    }


    // Constructor for cross identityStatement service responses
    ServiceInvocationStateful(Serializable returns, IdentityStatement serverIS, IdentityStatement crossIS, UUID nonce, CryptoOperations crOp) {
        this(null,returns,serverIS,crossIS,nonce,crOp,Long.MAX_VALUE);
    }

    // General constructor - private
    private ServiceInvocationStateful(Serializable parameters, Serializable returns, IdentityStatement localIS, IdentityStatement crossIS, UUID nonce, CryptoOperations crOp, long clockCounter) {
        so = new ServiceInvocationInternalObject();
        so.parameters = parameters;
        so.returns = returns;
        if (nonce == null)
            so.nonce = new UUID(System.currentTimeMillis(), (long)(Long.MAX_VALUE*Math.random())); // Protects for server replay
        else
            so.nonce = nonce;
        identityStatement = localIS;
        crossIdentityStatement = crossIS;
        so.issuingTimestamp = System.currentTimeMillis() + clockCounter; // For replay protection
        try {
            blob = crOp.sign(so);
        } catch (Exception e) { e.printStackTrace(); }
    }

    public void validateRequest(PublicKey trustAnchor, long clockCounter) throws GismoIdmException {
            validateRequest(trustAnchor, clockCounter,false);
    }
    public void validateRequest(PublicKey trustAnchor,long serverClockCounter,boolean acceptsExpiredIS) throws GismoIdmException {
        // Check validity of the identity statement
        System.out.println("Validating identityStatement " + identityStatement.toString());
        if (acceptsExpiredIS) 
            identityStatement.validateInferiorIdentityStatement(trustAnchor);
        else
            identityStatement.validateIdentityStatement(trustAnchor);
        System.out.println("IdentityStatement validated ok");
        PublicKey signerKey = identityStatement.getSubjectPublicKey();
        try {
            // Verify request signature
            if (!CryptoOperations.verify(blob,signerKey))
                throw new ServiceInvocationInvalid("Signature of request is not valid");
            // Verify timeliness of UUID
            // TODO don't rely entirely on clocks, keep a cache also
            long clockSkew = serverClockCounter - so.issuingTimestamp;
            System.out.println("Verifying expiration, clock skew=" + clockSkew);
            //if (lapse > MAX_CLOCK_SKEW) return false; // TODO fix this later - allow max clock skew
        } catch (Exception e) { e.printStackTrace(); }
    }
    
    public void validateResponse(UUID requestNonce,IdentityStatement crossIdentityStatement, 
            String serviceName, BooleanOperand reqs, CryptoOperations crOp) throws GismoIdmException {
        validateResponse(requestNonce, crossIdentityStatement, serviceName, reqs, crOp, false);
    }
    public void validateResponse(UUID requestNonce,IdentityStatement crossIdentityStatement,
            String serviceName, BooleanOperand reqs, CryptoOperations crOp,boolean acceptsExpiredIS) throws GismoIdmException {
        PublicKey trustAnchor = crOp.getTrustAnchor();
        try {
            if (acceptsExpiredIS) 
                identityStatement.validateInferiorIdentityStatement(trustAnchor);
            else
                identityStatement.validateIdentityStatement(trustAnchor);
        } catch (GismoIdmException e) {
            // Primary identityStatement may not validate if it is a cross identityStatement, in
            // which case the crossIdentityStatement should build a path to the trust anchor
            if (crossIdentityStatement == null) throw new ServiceInvocationInvalid(e.getMessage());
            crossIdentityStatement.validateIdentityStatement(trustAnchor);
            identityStatement.validateIdentityStatement(crossIdentityStatement.getSubjectPublicKey());
        }
        if (!identityStatement.validateSubjectName(serviceName)) 
            throw new ServiceInvocationInvalid("Identity of service is not " + serviceName + " as expected");
        PublicKey signerKey = identityStatement.getSubjectPublicKey();
        try {
            // Verify response signature
            if (!CryptoOperations.verify(blob,signerKey)) 
                throw new ServiceInvocationInvalid("Response signature is wrong");
            // Verify that UUID is the same as in the request
            if (!requestNonce.equals(so.nonce))
                throw new ServiceInvocationInvalid("Response nonce value is not the same as in the request");
            // Verify that the client's requirements are met by the service's IS
            SubjectProperties prop = new SubjectProperties(identityStatement);
            if (reqs == null) reqs = new TrueAlways();
            if (!reqs.getBooleanValue(prop.getAttributes()).truthValue())
                throw new ServiceInvocationInvalid("Response IS does not meet client's access requriements");
            
        } catch (java.security.NoSuchAlgorithmException e) { e.printStackTrace(); }
          catch (Exception e2) { e2.printStackTrace(); }
    }

    public IdentityStatement getIdentityStatement() { return identityStatement; }
    public Serializable getParameters() { return so.parameters; }
    public Serializable getReturns() { return so.returns; }
    public int getAuthenticationProtocol() { return ServiceInvocationFactory.STATEFUL;}
    public UUID getNonce() { return so.nonce; }

}
